﻿#region COPYRIGHT© 2009-2013 Phillip Clark. All rights reserved.

// For licensing information see License.txt (MIT style licensing).

#endregion

using System.Collections.Generic;
using FlitBit.Parse;

namespace FlitBit.Json
{
	public class JsonTokenizer : Tokenizer<JsonTokenKind>
	{
		public static readonly TokenSpec IdentifierTokenSpec = TokenSpec.MakeIdentifier("identifier", CharRule.Identifier)
																																		.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol);

		static readonly Match ExponentMatch = Match.MatchAny(
																							 Match.MatchExact(CharRule.MatchExactCaseInsensitive('E'), CharRule.MatchAny('-', '+')),
																							CharRule.MatchExactCaseInsensitive('E')
			);

		static readonly Match IntLiteralMatch = Match.MatchAny(CharRule.DecimalDigits,
																									Match.MatchExact(CharRule.MatchAny('-', '+'), CharRule.DecimalDigits));

		static readonly Match NumericLiteralMatchRule = Match.MatchAny(
																												 Match.MatchExact(IntLiteralMatch,
																																					Match.MatchExact(CharRule.MatchExact('.'), CharRule.DecimalDigits), ExponentMatch,
																																					CharRule.DecimalDigits),
																												Match.MatchExact(IntLiteralMatch,
																																				Match.MatchExact(CharRule.MatchExact('.'), CharRule.DecimalDigits)),
																												Match.MatchExact(IntLiteralMatch, ExponentMatch, CharRule.DecimalDigits),
																												IntLiteralMatch
			);

		public JsonTokenizer()
			: this(false)
		{}

		public JsonTokenizer(bool ignoreWhitespace)
		{
			IsIgnoringWhitespace = ignoreWhitespace;
			InitParser(ignoreWhitespace);
		}

		public bool IsIgnoringWhitespace { get; private set; }

		public override IEnumerable<Token<JsonTokenKind>> Tokenize(char[] characters)
		{
			var input = new TokenizerState<JsonTokenKind>(InnerTokenize(characters));
			while (input.MoveNext())
			{
				Token<JsonTokenKind> tk = input.Current;
				try
				{
					switch (tk.Kind)
					{
						case JsonTokenKind.Colon_Symbol:
							input.MustBeFollowedByAny(
																			 JsonTokenKind.StringLiteral,
																			JsonTokenKind.NumericLiteral,
																			JsonTokenKind.Left_Brace_Symbol,
																			JsonTokenKind.Left_Curly_Brace_Symbol,
																			JsonTokenKind.True,
																			JsonTokenKind.False,
																			JsonTokenKind.Null);
							break;
						case JsonTokenKind.Comma_Symbol:
							if (input.ScopeDepth == 0)
							{
								throw new ParseException(
									"Invalid comma found outside of a member list: ",
									tk.Capture.Position
									);
							}
							if (input.PeekScope<Token<JsonTokenKind>>()
											.Kind == JsonTokenKind.Left_Curly_Brace_Symbol)
							{
								// Inside of an object, on the left side of name:value pairs...
								var next = input.MustBeFollowedByAny(JsonTokenKind.StringLiteral, JsonTokenKind.Identifier);
								if (next.Kind == JsonTokenKind.StringLiteral)
								{
									input.MoveNext();
									input.InsertLookahead(new Token<JsonTokenKind>(IdentifierTokenSpec, JsonTokenKind.Identifier, next.Capture));
								}
							}
							else
							{
								input.MustBeFollowedByAny(
																				 JsonTokenKind.StringLiteral,
																				JsonTokenKind.NumericLiteral,
																				JsonTokenKind.Left_Brace_Symbol,
																				JsonTokenKind.Left_Curly_Brace_Symbol,
																				JsonTokenKind.True,
																				JsonTokenKind.False,
																				JsonTokenKind.Null);
							}
							break;
						case JsonTokenKind.Left_Brace_Symbol:
							input.PushScope(tk);
							input.MustBeFollowedByAny(
																			 JsonTokenKind.StringLiteral,
																			JsonTokenKind.NumericLiteral,
																			JsonTokenKind.Left_Brace_Symbol,
																			JsonTokenKind.Left_Curly_Brace_Symbol,
																			JsonTokenKind.True,
																			JsonTokenKind.False,
																			JsonTokenKind.Null,
																			JsonTokenKind.Right_Brace_Symbol
								);
							break;
						case JsonTokenKind.Left_Curly_Brace_Symbol:
							input.PushScope(tk);
							// Inside of an object, on the left side of name:value pairs...
							var first = input.MustBeFollowedByAny(JsonTokenKind.StringLiteral, JsonTokenKind.Identifier,
																										JsonTokenKind.Right_Curly_Brace_Symbol);
							if (first.Kind == JsonTokenKind.StringLiteral)
							{
								input.MoveNext();
								input.InsertLookahead(new Token<JsonTokenKind>(IdentifierTokenSpec, JsonTokenKind.Identifier, first.Capture));
							}
							break;
						case JsonTokenKind.Identifier:
							input.MustBeFollowedBy(JsonTokenKind.Colon_Symbol);
							break;
						case JsonTokenKind.Right_Curly_Brace_Symbol:
						case JsonTokenKind.Right_Brace_Symbol:
							input.PopScope();
							input.MustBeAtEndOrFollowedByAny(
																							 JsonTokenKind.Right_Brace_Symbol,
																							JsonTokenKind.Right_Curly_Brace_Symbol,
																							JsonTokenKind.Comma_Symbol);
							break;
						case JsonTokenKind.StringLiteral:
						case JsonTokenKind.NumericLiteral:
						case JsonTokenKind.Null:
						case JsonTokenKind.True:
						case JsonTokenKind.False:
							input.MustBeAtEndOrFollowedByAny(
																							 JsonTokenKind.Right_Brace_Symbol,
																							JsonTokenKind.Right_Curly_Brace_Symbol,
																							JsonTokenKind.Comma_Symbol);
							break;
					}
				}
				catch (ParseException ex)
				{
					tk = new Token<JsonTokenKind>(TokenSpec.ErrorSpec, ErrorKind,
																				new Capture(Match.NullMatch, tk.Capture.Position, tk.Capture.Value, ex.Message));
				}

				yield return tk;
			}
		}

		void InitParser(bool ignoreWhitespace)
		{
			if (ignoreWhitespace)
			{
				IgnoreWhitespace(JsonTokenKind.Whitespace, TokenSpec.MakeWhitespace("whitespace"));
			}
			else
			{
				SetWhitespaceSpec(JsonTokenKind.Whitespace, TokenSpec.MakeWhitespace("whitespace"));
			}

			SetIdentifierSpec(JsonTokenKind.Identifier, IdentifierTokenSpec);
			AddLiteralSpec(JsonTokenKind.StringLiteral,
										TokenSpec.MakeLiteral("string", Match.MatchAny(CharRule.DoubleQuotedString, CharRule.SingleQuotedString))
														.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol));
			AddLiteralSpec(JsonTokenKind.NumericLiteral, TokenSpec.MakeLiteral("numeric", NumericLiteralMatchRule)
																														.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol));
			AddTokenSpec(JsonTokenKind.Null, TokenSpec.MakeKeyword("null")
																								.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol));
			AddTokenSpec(JsonTokenKind.True, TokenSpec.MakeKeyword("true")
																								.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol));
			AddTokenSpec(JsonTokenKind.False, TokenSpec.MakeKeyword("false")
																								.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol));
			AddTokenSpec(JsonTokenKind.Colon_Symbol, TokenSpec.MakeSymbol("sym-colon", ':'));
			AddTokenSpec(JsonTokenKind.Comma_Symbol, TokenSpec.MakeSymbol("sym-comma", ','));
			AddTokenSpec(JsonTokenKind.Left_Brace_Symbol, TokenSpec.MakeSymbol("sym-left-brace", '[')
																														.WithScopeInfluence(TokenScopeInfluence.Open));
			AddTokenSpec(JsonTokenKind.Left_Curly_Brace_Symbol, TokenSpec.MakeSymbol("sym-left-curly", '{')
																																	.WithScopeInfluence(TokenScopeInfluence.Open));
			AddTokenSpec(JsonTokenKind.Right_Brace_Symbol, TokenSpec.MakeSymbol("sym-right-brace", ']')
																															.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol)
																															.WithScopeInfluence(TokenScopeInfluence.Close));
			AddTokenSpec(JsonTokenKind.Right_Curly_Brace_Symbol, TokenSpec.MakeSymbol("sym-right-curly", '}')
																																		.IsFollowedBy(TokenFollowedBy.WhitespaceOrSymbol)
																																		.WithScopeInfluence(TokenScopeInfluence.Close));
		}
	}
}